﻿using System;
using System.Collections.Generic;
using System.Text;

namespace DigitalQuran.Qnl
{
    /// <summary>
    /// QNL Parser
    /// </summary>
    public sealed class Parser
    {
        /// <summary>
        /// Gets the Scanner used to Scan string
        /// </summary>
        public Scanner Scanner { get; private set; }

        /// <summary>
        /// Create new instance of Parser
        /// </summary>
        public Parser()
        {
            Scanner = new Scanner();
        }

        /// <summary>
        /// Parse the input string and create parser tree
        /// </summary>
        /// <param name="input">String to parse</param>
        /// <returns>Returns ParseTree</returns>
        public ParseTree Parse(string input)
        {
            var tree = new ParseTree();
            try
            {
                ParseInternal(ref input, ref tree);

                if (tree.Values.Count == 0 && tree.Path.Count > 0)
                    tree.Values.Add(new ValueSpan(tree.Path[tree.Path.Count - 1].Value));

                tree.Values.Sort();
            }
            catch (QnlException ex) { tree.Errors.Add(ex); }
            catch (Exception ex) { tree.Errors.Add(ex); }

            return tree;
        }

        Patterns FootPrints = new Patterns();
        void ParseInternal(ref string input, ref ParseTree tree)
        {
            Scanner.Text = input;
            tree.Reset();
            using (var enm = Scanner.GetEnumerator())
            {
                Token objToken;
                QuranObjectType type;
                Pattern pattern = null;

                Token lastToken = null;
                while (enm.MoveNext())
                {
                    switch (enm.Current.Type)
                    {
                        case TokenType.Hyphen:
                        case TokenType.Comma:
                            do
                            {
                                switch (enm.Current.Type)
                                {
                                    case TokenType.Number:
                                        lastToken = enm.Current;
                                        break;
                                    case TokenType.Comma:
                                        if (lastToken == null)
                                            throw new QnlException("Comma Unexpected", enm.Current);

                                        tree.AddValue(lastToken, null);
                                        break;
                                    case TokenType.Hyphen:
                                        if (!enm.MoveNext()) return;
                                        if (enm.Current.Type != TokenType.Number)
                                            throw new QnlException("Number Expected", enm.Current);

                                        tree.AddValue(lastToken, enm.Current);

                                        if (!enm.MoveNext()) return;
                                        if (enm.Current.Type != TokenType.Comma)
                                            throw new QnlException("Comma Expected", enm.Current);
                                        break;
                                    default:
                                        throw new QnlException("Comma Seprated Range Values Expected", enm.Current);
                                }
                            } while (enm.MoveNext());
                            if (lastToken != null && lastToken.Type == TokenType.Number)
                                tree.AddValue(lastToken, null);
                            return;
                        case TokenType.Identifier:
                            break;
                        default:
                            throw new QnlException("Identifier Expected", enm.Current);                            
                    }

                    objToken = enm.Current;
                    type = DetermineObjType(enm.Current);
                    if (pattern == null)
                        pattern = FootPrints[type];
                    else
                        pattern = pattern.LookAhead;

                    while (pattern != null && pattern.ExpectedObject != type)
                        pattern = pattern.LookAhead;

                    if (pattern == null)
                        throw new QnlException("Unexpected Token", enm.Current);

                    if (!enm.MoveNext()) return;                    
                    if (enm.Current.Type != TokenType.Number)                            
                        throw new QnlException("Number Expected", enm.Current);

                    tree.AddPath(objToken, type, enm.Current);
                    lastToken = enm.Current;
                }            
            }
        }

        QuranObjectType DetermineObjType(Token t)
        {
            QuranObjectType type;
            if (!Enum.TryParse<QuranObjectType>(t.Text, true, out type))
            {
                throw new QnlException(string.Format("'{0}' is unexpected.", t.Text), t);
            }

            return type;
        }

        public int Compare(ValueSpan x, ValueSpan y)
        {
            return x.LowerLimit - y.LowerLimit;
        }
    }

    /// <summary>
    /// Part of Pattern to match
    /// </summary>
    sealed class Pattern
    {
        internal Pattern(QuranObjectType obj)
        {
            ExpectedObject = obj;
        }

        internal QuranObjectType ExpectedObject { get; private set; }

        internal Pattern LookAhead { get; set; }
    }

    sealed class Patterns : Dictionary<QuranObjectType, Pattern>
    {
        internal Patterns()
        {
            this.Add(QuranObjectType.Prostration, new Pattern(QuranObjectType.Prostration));
            this.Add(QuranObjectType.Letter, new Pattern(QuranObjectType.Letter));

            this.Add(QuranObjectType.Word, new Pattern(QuranObjectType.Word) { LookAhead = this[QuranObjectType.Letter] });
            this.Add(QuranObjectType.Verse, new Pattern(QuranObjectType.Verse) { LookAhead = this[QuranObjectType.Word] });

            this.Add(QuranObjectType.Page, new Pattern(QuranObjectType.Page) { LookAhead = this[QuranObjectType.Verse] });
            this.Add(QuranObjectType.Bowing, new Pattern(QuranObjectType.Bowing) { LookAhead = this[QuranObjectType.Verse] });

            this.Add(QuranObjectType.Chapter, new Pattern(QuranObjectType.Chapter) { LookAhead = this[QuranObjectType.Bowing] });
            this.Add(QuranObjectType.Station, new Pattern(QuranObjectType.Station) { LookAhead = this[QuranObjectType.Chapter] });

            this.Add(QuranObjectType.Group, new Pattern(QuranObjectType.Group) { LookAhead = this[QuranObjectType.Bowing] });
            this.Add(QuranObjectType.Quarter, new Pattern(QuranObjectType.Quarter) { LookAhead = this[QuranObjectType.Group] });
            this.Add(QuranObjectType.Part, new Pattern(QuranObjectType.Part) { LookAhead = this[QuranObjectType.Quarter] });
        }
    }
}